package org.osm2world.core.world.modules;

import static java.lang.Math.PI;
import static java.util.Arrays.asList;
import static org.osm2world.core.math.VectorXZ.fromAngle;
import static org.osm2world.core.target.common.material.NamedTexCoordFunction.*;
import static org.osm2world.core.target.common.material.TexCoordUtil.*;
import static org.osm2world.core.world.modules.common.WorldModuleGeometryUtil.createTriangleStripBetween;

import java.awt.Color;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

import org.osm2world.core.map_data.data.MapArea;
import org.osm2world.core.map_data.data.MapElement;
import org.osm2world.core.map_data.data.MapNode;
import org.osm2world.core.map_data.data.overlaps.MapOverlap;
import org.osm2world.core.map_elevation.data.EleConnectorGroup;
import org.osm2world.core.map_elevation.data.GroundState;
import org.osm2world.core.math.LineSegmentXZ;
import org.osm2world.core.math.PolygonXYZ;
import org.osm2world.core.math.SimplePolygonXZ;
import org.osm2world.core.math.TriangleXYZ;
import org.osm2world.core.math.TriangleXZ;
import org.osm2world.core.math.VectorXYZ;
import org.osm2world.core.math.VectorXZ;
import org.osm2world.core.math.algorithms.JTSTriangulationUtil;
import org.osm2world.core.math.algorithms.Poly2TriTriangulationUtil;
import org.osm2world.core.target.RenderableToAllTargets;
import org.osm2world.core.target.Target;
import org.osm2world.core.target.common.material.ImmutableMaterial;
import org.osm2world.core.target.common.material.Material;
import org.osm2world.core.target.common.material.Material.Interpolation;
import org.osm2world.core.target.common.material.Materials;
import org.osm2world.core.util.MinMaxUtil;
import org.osm2world.core.util.exception.TriangulationException;
import org.osm2world.core.world.data.AbstractAreaWorldObject;
import org.osm2world.core.world.data.TerrainBoundaryWorldObject;
import org.osm2world.core.world.modules.SurfaceAreaModule.SurfaceArea;
import org.osm2world.core.world.modules.common.AbstractModule;

import com.google.common.base.Function;

/**
 * adds golf courses to the map
 */
public class GolfModule extends AbstractModule {
	
	private static final int HOLE_CIRCLE_VERTICES = 8;
	private static final double HOLE_RADIUS = 0.108 / 2;
	private static final double HOLE_DEPTH = 0.102;
	
	@Override
	public void applyToArea(MapArea area) {
		
		if (!area.getTags().containsKey("golf")) return;
		
		if (area.getTags().contains("golf", "tee")) {
			area.addRepresentation(new Tee(area));
		} else if (area.getTags().contains("golf", "fairway")) {
			area.addRepresentation(new Fairway(area));
		} else if (area.getTags().contains("golf", "green")) {
			area.addRepresentation(new Green(area));
		}
		
	}
	
	private static class Tee extends SurfaceArea {

		private Tee(MapArea area) {
			
			super(area, area.getTags().containsKey("surface")
					? area.getTags().getValue("surface")
					: "grass");
			
		}
		
	}
	
	private static class Fairway extends SurfaceArea {

		private Fairway(MapArea area) {
			
			super(area, area.getTags().containsKey("surface")
					? area.getTags().getValue("surface")
					: "grass");
			
		}
		
	}
	
	private static class Green extends AbstractAreaWorldObject
			implements RenderableToAllTargets, TerrainBoundaryWorldObject {
		
		private final VectorXZ pinPosition;
		private final SimplePolygonXZ pinHoleLoop;
		private final EleConnectorGroup pinConnectors;
		
		private Green(MapArea area) {
			
			super(area);
			
			/* check whether a pin has been explicitly mapped */
			
			VectorXZ explicitPinPosition = null;
			
			for (MapOverlap<?, ?> overlap : area.getOverlaps()) {
				
				MapElement other = overlap.getOther(area);
				
				if (other.getTags().contains("golf","pin")
						&& other instanceof MapNode) {
					
					explicitPinPosition = ((MapNode)other).getPos();
					
					break;
					
				}
				
			}
			
			/* place an implicit pin if none has been mapped */
			
			if (explicitPinPosition != null) {
				
				pinPosition = explicitPinPosition;
				
			} else {
				
				pinPosition = area.getOuterPolygon().getCenter();
				
			}
				
			/* create circle around the hole */
				
			List<VectorXZ> holeRing = new ArrayList<VectorXZ>(HOLE_CIRCLE_VERTICES);
			
			for (int i = 0; i < HOLE_CIRCLE_VERTICES; i++) {
				VectorXZ direction = fromAngle(2 * PI * ((double)i / HOLE_CIRCLE_VERTICES));
				VectorXZ vertex = pinPosition.add(direction.mult(HOLE_RADIUS));
				holeRing.add(vertex);
			}
			
			holeRing.add(holeRing.get(0));
			
			pinHoleLoop = new SimplePolygonXZ(holeRing);
			
			pinConnectors = new EleConnectorGroup();
			pinConnectors.addConnectorsFor(pinHoleLoop.getVertexCollection(), area, GroundState.ON);
			
		}
		
		@Override
		public GroundState getGroundState() {
			return GroundState.ON;
		}
		
		@Override
		public EleConnectorGroup getEleConnectors() {
			
			EleConnectorGroup eleConnectors = super.getEleConnectors();
			
			if (pinConnectors != null) {
				eleConnectors.addAll(pinConnectors);
			}
			
			return eleConnectors;
			
		}
		
		@Override
		public void renderTo(Target<?> target) {
			
			/* render green surface */
			
			String surfaceValue = area.getTags().getValue("surface");
			
			Material material = Materials.GRASS;
			
			if (surfaceValue != null && !"grass".equals(surfaceValue)) {
				material = Materials.getSurfaceMaterial(surfaceValue, material);
			}

			Collection<TriangleXZ> trianglesXZ = getGreenTriangulation();
			Collection<TriangleXYZ> triangles = getEleConnectors().getTriangulationXYZ(trianglesXZ);
			
			target.drawTriangles(material, triangles,
					triangleTexCoordLists(triangles , material, GLOBAL_X_Z));
			
			/* render pin */
					
			PolygonXYZ upperHoleRing = pinConnectors.getPosXYZ(pinHoleLoop);
				
			drawPin(target, pinPosition, upperHoleRing.getVertexLoop());
			
		}
		
		private List<TriangleXZ> getGreenTriangulation() {
			
			List<SimplePolygonXZ> holes = area.getPolygon().getHoles();
			
			holes.add(pinHoleLoop);
			
			try {
				
				return Poly2TriTriangulationUtil.triangulate(
					area.getPolygon().getOuter(),
					holes,
					Collections.<LineSegmentXZ>emptyList(),
					Collections.<VectorXZ>emptyList());
				
			} catch (TriangulationException e) {
				
				System.err.println("Poly2Tri exception for " + this + ":");
				e.printStackTrace();
				System.err.println("... falling back to JTS triangulation.");
				
				return JTSTriangulationUtil.triangulate(
						area.getPolygon().getOuter(),
						holes,
						Collections.<LineSegmentXZ>emptyList(),
						Collections.<VectorXZ>emptyList());
				
			}
			
		}

		private static void drawPin(Target<?> target,
				VectorXZ pos, List<VectorXYZ> upperHoleRing) {
			
			double minHoleEle = MinMaxUtil.<VectorXYZ>min(upperHoleRing,
					new Function<VectorXYZ, Double>() {
				@Override public Double apply(VectorXYZ v) {
					return v.y;
				}
			}).y;
			
			double holeBottomEle = minHoleEle - HOLE_DEPTH;
						
			/* draw hole */
			
			List<VectorXYZ> lowerHoleRing = new ArrayList<VectorXYZ>();
			for (VectorXYZ v : upperHoleRing) {
				lowerHoleRing.add(v.y(holeBottomEle));
			}
									
			List<VectorXYZ> vs = createTriangleStripBetween(
					upperHoleRing, lowerHoleRing);
			
			Material groundMaterial = Materials.EARTH.makeSmooth();
			
			target.drawTriangleStrip(groundMaterial, vs,
					texCoordLists(vs, groundMaterial, STRIP_WALL));
			
			target.drawConvexPolygon(groundMaterial, lowerHoleRing,
					texCoordLists(vs, groundMaterial, GLOBAL_X_Z));
			
			/* draw flag */
			
			target.drawColumn(Materials.PLASTIC_GREY.makeSmooth(), null,
					pos.xyz(holeBottomEle), 1.5, 0.007, 0.007, false, true);
			
			ImmutableMaterial flagcloth = new ImmutableMaterial(Interpolation.SMOOTH, Color.YELLOW);
			
			List<VectorXYZ> flagVertices = asList(
					new VectorXYZ(pos.x, 1.5, pos.z),
					new VectorXYZ(pos.x, 1.2, pos.z),
					new VectorXYZ(pos.x + 0.4, 1.5, pos.z),
					new VectorXYZ(pos.x + 0.4, 1.2, pos.z));
			
			target.drawTriangleStrip(flagcloth, flagVertices,
					texCoordLists(flagVertices, flagcloth, STRIP_WALL));
			
		}
		
	}
	
}
